// $Id: session.c 52 2011-04-20 17:06:01Z cedric.shih@gmail.com $
/*
 * Copyright (C) 2010 Cedric Shih <cedric.shih@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <time.h>

#ifdef WIN32
#include <winsock2.h>
#else
#include <sys/socket.h>
#include <arpa/inet.h>
#endif

#include <event.h>
#include <nio.h>
#include <nio/buffer.h>
#include <nio/socket.h>

#include "session.h"
#include "util.h"

#include "niohttp/test.h"

#define BUFFER_SIZE 32768
#define PORT 8888
#define COUNT 2

static void test_server_accept_cb(int fd, short event, void *arg);
static void test_submit(niohttp_request_cb cb);
static void random_data(char *data, int len);

static const char *version;
static struct event_base *evbase;
static struct nioreactor *reactor;
static struct niobuffer *content;
static struct niobuffer *buffer;
static struct niosocket *sock;
static struct niohttp_session *session;
static struct niohttp_request *req;
static int counter;

enum server_mode {
	server_normal,
	server_sanity,
};

static struct {
	unsigned short port;
	enum server_mode mode;
	struct evbuffer *expect;
	struct evbuffer *actual;
	struct {
		int sock;
		struct event ev;
	} accept;
	struct {
		int sock;
		struct event ev;
	} conn;
} server;

static struct {
	struct in_addr address;
	unsigned short port;
} client;

static void test_setup()
{
	struct timeval tv;

	memset(&server, '\0', sizeof(server));

	evbase = event_base_new();
	reactor = nioreactor_new(evbase);
	content = niobuffer_new(BUFFER_SIZE);
	buffer = niobuffer_new(BUFFER_SIZE);
	sock = niosocket_new();
	session = niohttp_session_new(reactor, buffer, sock);
	req = niohttp_request_new();

	server.expect = evbuffer_new();
	server.actual = evbuffer_new();

	gettimeofday(&tv, NULL);
	srand(tv.tv_usec);
	server.port = PORT + (rand() % (65535 - PORT));

	server.accept.sock = niohttp_test_bind(server.port);
	TEST_ASSERT(server.accept.sock > 0);

	event_set(&server.accept.ev, server.accept.sock, EV_READ|EV_PERSIST,
			test_server_accept_cb, NULL);
	event_base_set(evbase, &server.accept.ev);
	event_add(&server.accept.ev, NULL);

	counter = 0;
}

static void test_teardown()
{
#ifndef WIN32
	fsync(server.accept.sock);
#endif
	close(server.accept.sock);

	evbuffer_free(server.actual);
	evbuffer_free(server.expect);

	niohttp_request_free(req);
	niosocket_free(sock);
	niohttp_session_free(session);
	niobuffer_free(buffer);
	niobuffer_free(content);
	nioreactor_free(reactor);
	event_base_free(evbase);
}

static void test_init()
{
	TEST_ASSERT_NOT_NULL(evbase);
	TEST_ASSERT_NOT_NULL(reactor);
	TEST_ASSERT_NOT_NULL(buffer);
	TEST_ASSERT_NOT_NULL(sock);
	TEST_ASSERT_NOT_NULL(session);
	TEST_ASSERT_NOT_NULL(req);

	TEST_ASSERT(event_initialized(&server.accept.ev));
	TEST_ASSERT(event_pending(&server.accept.ev, EV_READ, NULL));
}

static const char *req_meta = "POST /index.html HTTP/%s\r\n"
		"Content-Length: %d\r\n"
		"Counter: %d\r\n"
		"Host: www.example.com\r\n"
		"\r\n";

static const char *req_data = "0123456789";

static const char *resp_meta = "HTTP/%s 200 OK\r\n"
		"Counter: %d\r\n"
		"Date: Mon, 23 May 2005 22:38:34 GMT\r\n"
		"Server: Apache/1.3.3.7 (Unix)  (Red-Hat/Linux)\r\n"
		"Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT\r\n"
		"Etag: \"3f80f-1b6-3e1cb03b\"\r\n"
		"Accept-Ranges: bytes\r\n"
		"Content-Length: %d\r\n"
		"Connection: close\r\n"
		"Content-Type: text/html; charset=UTF-8\r\n"
		"\r\n";

static const char *resp_data = "<html>\n"
		"<head>\n"
		"<title>Example Page</title>\n"
		"</head>\n"
		"<body>\n"
		"<h1>Example Page</h1>\n"
		"<p>This is an example.</p>\n"
		"</body>\n"
		"</html>\n";

static void test_server_socket_cb(int fd, short event, void *arg)
{
	int rc, l, len, i;
	char buffer[256];

	TEST_ASSERT(!(event & EV_TIMEOUT));

	evbuffer_drain(server.expect, EVBUFFER_LENGTH(server.expect));
	evbuffer_drain(server.actual, EVBUFFER_LENGTH(server.actual));

	if (event & EV_READ) {
		l = evbuffer_read(server.actual, fd, -1);

		len = evbuffer_add_printf(server.expect, req_meta,
				version, strlen(req_data), counter);
		len += evbuffer_add_printf(server.expect, "%s", req_data);

		TEST_ASSERT_EQUAL_INT(len, l);
		TEST_ASSERT(!(strncmp((char *)EVBUFFER_DATA(server.expect),
				(char *)EVBUFFER_DATA(server.actual), len)));

		event_set(&server.conn.ev, server.conn.sock, EV_WRITE,
				test_server_socket_cb, NULL);

		rc = event_base_set(evbase, &server.conn.ev);
		TEST_ASSERT_EQUAL_INT(0, rc);

		rc = event_add(&server.conn.ev, NULL);
		TEST_ASSERT_EQUAL_INT(0, rc);
	} else if (event & EV_WRITE) {
		switch (server.mode) {
		case server_normal:
			len = evbuffer_add_printf(server.expect, resp_meta,
					version, counter, strlen(resp_data));
			len += evbuffer_add_printf(server.expect, resp_data,
					counter);
			l = write(fd, EVBUFFER_DATA(server.expect), len);

			TEST_ASSERT_EQUAL_INT(len, l);

			if (!strcmp("1.1", version)) {
				event_set(&server.conn.ev, server.conn.sock,
						EV_READ,
						test_server_socket_cb, NULL);

				rc = event_base_set(evbase, &server.conn.ev);
				TEST_ASSERT_EQUAL_INT(0, rc);

				rc = event_add(&server.conn.ev, NULL);
				TEST_ASSERT_EQUAL_INT(0, rc);
			} else {
				rc = close(fd);
				TEST_ASSERT_EQUAL_INT(0, rc);
			}
			break;
		case server_sanity:
			for (i = 0; i < COUNT; i++) {
				random_data(buffer, sizeof(buffer));
				l = write(fd, buffer, sizeof(buffer));
			}
			break;
		}
	} else {
		TEST_FAIL("unexpected event");
	}
}

static void test_server_accept_cb(int fd, short event, void *arg)
{
	int rc;
	struct sockaddr_in s_in;
#ifdef WIN32
	int len;
#else
	socklen_t len;
#endif

	len = sizeof(s_in);

	server.conn.sock = accept(fd, (struct sockaddr *) &s_in, &len);
	TEST_ASSERT(server.conn.sock > 0);

	event_set(&server.conn.ev, server.conn.sock, EV_READ,
			test_server_socket_cb, NULL);

	rc = event_base_set(evbase, &server.conn.ev);
	TEST_ASSERT_EQUAL_INT(0, rc);

	rc = event_add(&server.conn.ev, NULL);
	TEST_ASSERT_EQUAL_INT(0, rc);
}

static void test_content_cb(struct niohttp_session *session,
		struct niosocket *socket, void *cbarg);

static void test_request_done(void);

static void test_request_cb(struct niohttp_request *req,
		struct niohttp_response *resp, void *cbarg)
{
	int rc, len;
	char cbuffer[32];
	const char *clen = cbuffer;

	len = strlen(resp_data);
	snprintf(cbuffer, sizeof(cbuffer), "%d", len);

	TEST_ASSERT_EQUAL_STRING("POST", niohttp_request_method(req));
	TEST_ASSERT_EQUAL_STRING("/index.html", niohttp_request_uri(req));
	TEST_ASSERT_EQUAL_INT(200, niohttp_response_code(resp));
	TEST_ASSERT_EQUAL_STRING("OK", niohttp_response_reason(resp));

	TEST_ASSERT_EQUAL_STRING("Mon, 23 May 2005 22:38:34 GMT",
			niohttp_response_header(resp, "Date"));
	TEST_ASSERT_EQUAL_STRING("Apache/1.3.3.7 (Unix)  (Red-Hat/Linux)",
			niohttp_response_header(resp, "Server"));
	TEST_ASSERT_EQUAL_STRING("Wed, 08 Jan 2003 23:11:55 GMT",
			niohttp_response_header(resp, "Last-Modified"));
	TEST_ASSERT_EQUAL_STRING("\"3f80f-1b6-3e1cb03b\"",
			niohttp_response_header(resp, "Etag"));
	TEST_ASSERT_EQUAL_STRING("bytes",
			niohttp_response_header(resp, "Accept-Ranges"));
	TEST_ASSERT_EQUAL_STRING(clen,
			niohttp_response_header(resp, "Content-Length"));
	TEST_ASSERT_EQUAL_STRING("close",
			niohttp_response_header(resp, "Connection"));
	TEST_ASSERT_EQUAL_STRING("text/html; charset=UTF-8",
			niohttp_response_header(resp, "Content-Type"));

	if (niobuffer_remaining(buffer) < len) {
		niobuffer_compact(buffer);

		rc = niobuffer_set_limit(buffer, len);
		TEST_ASSERT_EQUAL_INT(0, rc);

		rc = niohttp_session_readable(session, test_content_cb, NULL);
		TEST_ASSERT_EQUAL_INT(0, rc);
	} else {
		test_request_done();
	}
}

static void test_content_cb(struct niohttp_session *session,
		struct niosocket *socket, void *cbarg)
{
	int rc;

	rc = niobuffer_read(buffer, socket);
	TEST_ASSERT(rc > 0);

	if (niobuffer_remaining(buffer) > 0) {
		rc = niohttp_session_readable(session, test_content_cb, NULL);
		TEST_ASSERT_EQUAL_INT(0, rc);
		return;
	} else {
		niobuffer_flip(buffer);
		test_request_done();
	}
}

static void test_request_done(void)
{
	int rc;

	TEST_ASSERT(!strncmp(resp_data, (char *)niobuffer_array(buffer) +
			niobuffer_position(buffer),
			niobuffer_remaining(buffer)));

	if (!strcmp("1.0", version)) {
		rc = niosocket_close(sock);
		TEST_ASSERT_EQUAL_INT(0, rc);
	}

	counter++;

	if (counter < COUNT) {
		test_submit(test_request_cb);
	} else {
		rc = event_del(&server.accept.ev);
		TEST_ASSERT_EQUAL_INT(0, rc);

		rc = event_del(&server.conn.ev);
		TEST_ASSERT_EQUAL_INT(0, rc);

		if (!strcmp("1.1", version)) {
			rc = niosocket_close(sock);
			TEST_ASSERT_EQUAL_INT(0, rc);
		}
	}
}

static void test_submit(niohttp_request_cb cb)
{
	int rc;
	char buffer[32];

	if (counter == 0 || !strcmp("1.0", version)) {
		rc = niosocket_connect(sock, &client.address,
				client.port);
		TEST_ASSERT_EQUAL_INT(0, rc);
	}

	niobuffer_clear(content);

	rc = niobuffer_putf(content, "%s", req_data);
	TEST_ASSERT_EQUAL_INT(0, rc);

	niobuffer_flip(content);

	rc = niohttp_request_set_protocol(req, "HTTP");
	TEST_ASSERT_EQUAL_INT(0, rc);

	rc = niohttp_request_set_version(req, version);
	TEST_ASSERT_EQUAL_INT(0, rc);

	rc = niohttp_request_set_method(req, "POST");
	TEST_ASSERT_EQUAL_INT(0, rc);

	rc = niohttp_request_set_uri(req, "/index.html");
	TEST_ASSERT_EQUAL_INT(0, rc);

	rc = niohttp_request_set_header(req, "Host", "www.example.com");
	TEST_ASSERT_EQUAL_INT(0, rc);

	snprintf(buffer, sizeof(buffer), "%d", counter);
	rc = niohttp_request_set_header(req, "Counter", buffer);
	TEST_ASSERT_EQUAL_INT(0, rc);

	rc = niohttp_session_submit(session, req, content, cb, NULL);
	TEST_ASSERT_EQUAL_INT(0, rc);
}

static void test_no_content()
{
	int rc;

	rc = inet_pton(AF_INET, "127.0.0.1", &client.address);
	TEST_ASSERT(rc > 0);

	client.port = server.port;
	version = "1.0";

	test_submit(test_request_cb);

	rc = nioreactor_loop(reactor, NULL);
	TEST_ASSERT_EQUAL_INT(0, rc);

	TEST_ASSERT_EQUAL_INT(COUNT, counter);
}

static void test_keep_alive()
{
	int rc;

	rc = inet_pton(AF_INET, "127.0.0.1", &client.address);
	TEST_ASSERT(rc > 0);

	client.port = server.port;
	version = "1.1";

	test_submit(test_request_cb);

	rc = nioreactor_loop(reactor, NULL);
	TEST_ASSERT_EQUAL_INT(0, rc);

	TEST_ASSERT_EQUAL_INT(COUNT, counter);
}

static void test_refuse_request_cb(struct niohttp_request *req,
		struct niohttp_response *resp, void *cbarg)
{
	int rc;

	TEST_ASSERT_EQUAL_STRING("POST", niohttp_request_method(req));
	TEST_ASSERT_EQUAL_STRING("/index.html", niohttp_request_uri(req));
	TEST_ASSERT_EQUAL_INT(-1, niohttp_response_code(resp));
	TEST_ASSERT_NULL(niohttp_response_reason(resp));

	if (!strcmp("1.0", version)) {
		rc = niosocket_close(sock);
		TEST_ASSERT_EQUAL_INT(0, rc);
	}

	counter++;

	if (counter < COUNT) {
		test_submit(test_refuse_request_cb);
	} else {
		rc = event_del(&server.accept.ev);
		TEST_ASSERT_EQUAL_INT(0, rc);
	}
}

static void test_refuse()
{
	int rc;

	rc = inet_pton(AF_INET, "127.0.0.1", &client.address);
	TEST_ASSERT(rc > 0);

	client.port = PORT + (rand() % (65535 - PORT));
	version = "1.0";

	test_submit(test_refuse_request_cb);

	rc = nioreactor_loop(reactor, NULL);
	TEST_ASSERT_EQUAL_INT(0, rc);

	TEST_ASSERT_EQUAL_INT(COUNT, counter);
}

static void test_sanity_request_cb(struct niohttp_request *req,
		struct niohttp_response *resp, void *cbarg)
{
	int rc;

	TEST_ASSERT_EQUAL_STRING("POST", niohttp_request_method(req));
	TEST_ASSERT_EQUAL_STRING("/index.html", niohttp_request_uri(req));
	TEST_ASSERT_EQUAL_INT(-1, niohttp_response_code(resp));
	TEST_ASSERT_NULL(niohttp_response_reason(resp));

	if (!strcmp("1.0", version)) {
		rc = niosocket_close(sock);
		TEST_ASSERT_EQUAL_INT(0, rc);
	}

	counter++;

	if (counter < COUNT) {
		test_submit(test_sanity_request_cb);
	} else {
		rc = event_del(&server.accept.ev);
		TEST_ASSERT_EQUAL_INT(0, rc);
	}
}

static void test_sanity()
{
	int rc;

	server.mode = server_sanity;

	rc = inet_pton(AF_INET, "127.0.0.1", &client.address);
	TEST_ASSERT(rc > 0);

	client.port = server.port;
	version = "1.0";

	test_submit(test_sanity_request_cb);

	rc = nioreactor_loop(reactor, NULL);
	TEST_ASSERT_EQUAL_INT(0, rc);

	TEST_ASSERT_EQUAL_INT(COUNT, counter);
}

void random_data(char *data, int len)
{
	int i;

	for (i = 0; i < len; i ++) {
		data[i] = (rand() % 128);
	}
}

TestRef niohttp_session_test()
{
	EMB_UNIT_TESTFIXTURES(fixtures) {
		new_TestFixture("niohttp_session_test_init", test_init),
		new_TestFixture("niohttp_session_test_no_content", test_no_content),
		new_TestFixture("niohttp_session_test_keep_alive", test_keep_alive),
		new_TestFixture("niohttp_session_refuse_test", test_refuse),
		new_TestFixture("niohttp_session_sanity_test", test_sanity),
	};
	EMB_UNIT_TESTCALLER(test_case, "niohttp_session_test", test_setup, test_teardown, fixtures);
	return (TestRef)&test_case;
}
